#!/usr/bin/env perl
#
# CDDL HEADER START
#
# The contents of this file are subject to the terms of the
# Common Development and Distribution License (the "License").
# You may not use this file except in compliance with the License.
#
# You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
# or http://www.opensolaris.org/os/licensing.
# See the License for the specific language governing permissions
# and limitations under the License.
#
# When distributing Covered Code, include this CDDL HEADER in each
# file and include the License file at usr/src/OPENSOLARIS.LICENSE.
# If applicable, add the following below this CDDL HEADER, with the
# fields enclosed by brackets "[]" replaced with your own identifying
# information: Portions Copyright [yyyy] [name of copyright owner]
#
# CDDL HEADER END
#
#
# Copyright 2008 Sun Microsystems, Inc.  All rights reserved.
# Use is subject to license terms.
#
# Copyright 2011 Joyent, Inc. All rights reserved.
#
# jsstyle - check for some common stylistic errors.
#
#   jsstyle is a sort of "lint" for Javascript coding style.  This tool is
#   derived from the cstyle tool, used to check for the style used in the
#   Solaris kernel, sometimes known as "Bill Joy Normal Form".
#
#   There's a lot this can't check for, like proper indentation of code
#   blocks.  There's also a lot more this could check for.
#
#   A note to the non perl literate:
#
#       perl regular expressions are pretty much like egrep
#       regular expressions, with the following special symbols
#
#       \s  any space character
#       \S  any non-space character
#       \w  any "word" character [a-zA-Z0-9_]
#       \W  any non-word character
#       \d  a digit [0-9]
#       \D  a non-digit
#       \b  word boundary (between \w and \W)
#       \B  non-word boundary
#

require 5.0;
use IO::File;
use Getopt::Std;
use strict;

my $usage =
"Usage: jsstyle [-h?vcC] [-t <num>] [-f <path>] [-o <config>] file ...

Check your JavaScript file for style.
See <https://github.com/davepacheco/jsstyle> for details on config options.
Report bugs to <https://github.com/davepacheco/jsstyle/issues>.

Options:
    -h  print this help and exit
    -v  verbose

    -c  check continuation indentation inside functions
    -t  specify tab width for line length calculation
    -C  don't check anything in header block comments

    -f PATH
        path to a jsstyle config file
    -o OPTION1,OPTION2
        set config options, e.g. '-o doxygen,indent=2'

";

my %opts;

if (!getopts("ch?o:t:f:vC", \%opts)) {
    print $usage;
    exit 2;
}

if (defined($opts{'h'}) || defined($opts{'?'})) {
    print $usage;
    exit;
}

my $check_continuation = $opts{'c'};
my $verbose = $opts{'v'};
my $ignore_hdr_comment = $opts{'C'};
my $tab_width = $opts{'t'};

# By default, tabs are 8 characters wide
if (! defined($opts{'t'})) {
    $tab_width = 8;
}


# Load config
my %config = (
    indent => "tab",
    "line-length" => 80,
    doxygen => 0,   # doxygen comments: /** ... */
    splint => 0,    # splint comments. Needed?
    "unparenthesized-return" => 1,
    "literal-string-quote" => "single",  # 'single' or 'double'
    "blank-after-start-comment" => 1,
    "blank-after-open-comment" => 1,
    "no-blank-for-anon-function" => 0,
    "continuation-at-front" => 0,
    "leading-right-paren-ok" => 0,
    "strict-indent" => 0
);
sub add_config_var ($$) {
    my ($scope, $str) = @_;

    if ($str !~ /^([\w-]+)(?:\s*=\s*(.*?))?$/) {
        die "$scope: invalid option: '$str'";
    }
    my $name = $1;
    my $value = ($2 eq '' ? 1 : $2);
    #print "scope: '$scope', str: '$str', name: '$name', value: '$value'\n";

    # Validate config var.
    if ($name eq "indent") {
        # A number of spaces or "tab".
        if ($value !~ /^\d+$/ && $value ne "tab") {
            die "$scope: invalid '$name': must be a number (of ".
                "spaces) or 'tab'";
        }
    } elsif ($name eq "line-length") { # numeric vars
        if ($value !~ /^\d+$/) {
            die "$scope: invalid '$name': must be a number";
        }
    } elsif ($name eq "doxygen" ||   # boolean vars
         $name eq "splint" ||
         $name eq "unparenthesized-return" ||
         $name eq "continuation-at-front" ||
         $name eq "leading-right-paren-ok" ||
         $name eq "leading-comma-ok" ||
         $name eq "uncuddled-else-ok" ||
         $name eq "whitespace-after-left-paren-ok" ||
         $name eq "strict-indent" ||
         $name eq "blank-after-open-comment" ||
         $name eq "blank-after-start-comment" ||
         $name eq "no-blank-for-anon-function") {

        if ($value != 1 && $value != 0) {
            die "$scope: invalid '$name': don't give a value";
        }
    } elsif ($name eq "literal-string-quote") {
        if ($value !~ /single|double/) {
            die "$scope: invalid '$name': must be 'single' ".
                "or 'double'";
        }
    } else {
        die "$scope: unknown config var: $name";
    }
    $config{$name} = $value;
}

if (defined($opts{'f'})) {
    my $path = $opts{'f'};
    my $fh = new IO::File $path, "r";
    if (!defined($fh)) {
        die "cannot open config path '$path'";
    }
    my $line = 0;
    while (<$fh>) {
        $line++;
        s/^\s*//;  # drop leading space
        s/\s*$//;  # drop trailing space
        next if ! $_;  # skip empty line
        next if /^#/;  # skip comments
        add_config_var "$path:$line", $_;
    }
}

if (defined($opts{'o'})) {
    for my $x (split /,/, $opts{'o'}) {
        add_config_var "'-o' option", $x;
    }
}


my ($filename, $line, $prev);       # shared globals

my $fmt;
my $hdr_comment_start;

if ($verbose) {
    $fmt = "%s: %d: %s\n%s\n";
} else {
    $fmt = "%s: %d: %s\n";
}

if ($config{"doxygen"}) {
    # doxygen comments look like "/*!" or "/**"; allow them.
    $hdr_comment_start = qr/^\s*\/\*[\!\*]?$/;
} else {
    $hdr_comment_start = qr/^\s*\/\*$/;
}

# Note, following must be in single quotes so that \s and \w work right.
my $lint_re = qr/\/\*(?:
    jsl:\w+?|ARGSUSED[0-9]*|NOTREACHED|LINTLIBRARY|VARARGS[0-9]*|
    CONSTCOND|CONSTANTCOND|CONSTANTCONDITION|EMPTY|
    FALLTHRU|FALLTHROUGH|LINTED.*?|PRINTFLIKE[0-9]*|
    PROTOLIB[0-9]*|SCANFLIKE[0-9]*|JSSTYLED.*?
    )\*\//x;

my $splint_re = qr/\/\*@.*?@\*\//x;

my $err_stat = 0;       # exit status

if ($#ARGV >= 0) {
    foreach my $arg (@ARGV) {
        my $fh = new IO::File $arg, "r";
        if (!defined($fh)) {
            printf "%s: cannot open\n", $arg;
        } else {
            &jsstyle($arg, $fh);
            close $fh;
        }
    }
} else {
    &jsstyle("<stdin>", *STDIN);
}
exit $err_stat;

my $no_errs = 0;        # set for JSSTYLED-protected lines

sub err($) {
    my ($error) = @_;
    unless ($no_errs) {
        printf $fmt, $filename, $., $error, $line;
        $err_stat = 1;
    }
}

sub err_prefix($$) {
    my ($prevline, $error) = @_;
    my $out = $prevline."\n".$line;
    unless ($no_errs) {
        printf $fmt, $filename, $., $error, $out;
        $err_stat = 1;
    }
}

sub err_prev($) {
    my ($error) = @_;
    unless ($no_errs) {
        printf $fmt, $filename, $. - 1, $error, $prev;
        $err_stat = 1;
    }
}

sub jsstyle($$) {

my ($fn, $filehandle) = @_;
$filename = $fn;            # share it globally

my $in_cpp = 0;
my $next_in_cpp = 0;

my $in_comment = 0;
my $in_header_comment = 0;
my $comment_done = 0;
my $in_function = 0;
my $in_function_header = 0;
my $in_declaration = 0;
my $note_level = 0;
my $nextok = 0;
my $nocheck = 0;

my $in_string = 0;

my ($okmsg, $comment_prefix);

$line = '';
$prev = '';
reset_indent();

line: while (<$filehandle>) {
    s/\r?\n$//; # strip return and newline

    # save the original line, then remove all text from within
    # double or single quotes, we do not want to check such text.

    $line = $_;

    #
    # C allows strings to be continued with a backslash at the end of
    # the line.  We translate that into a quoted string on the previous
    # line followed by an initial quote on the next line.
    #
    # (we assume that no-one will use backslash-continuation with character
    # constants)
    #
    $_ = '"' . $_       if ($in_string && !$nocheck && !$in_comment);

    #
    # normal strings and characters
    #
    s/'([^\\']|\\.)*'/\'\'/g;
    s/"([^\\"]|\\.)*"/\"\"/g;

    #
    # detect string continuation
    #
    if ($nocheck || $in_comment) {
        $in_string = 0;
    } else {
        #
        # Now that all full strings are replaced with "", we check
        # for unfinished strings continuing onto the next line.
        #
        $in_string =
            (s/([^"](?:"")*)"([^\\"]|\\.)*\\$/$1""/ ||
            s/^("")*"([^\\"]|\\.)*\\$/""/);
    }

    #
    # figure out if we are in a cpp directive
    #
    $in_cpp = $next_in_cpp || /^\s*#/;  # continued or started
    $next_in_cpp = $in_cpp && /\\$/;    # only if continued

    # strip off trailing backslashes, which appear in long macros
    s/\s*\\$//;

    # an /* END JSSTYLED */ comment ends a no-check block.
    if ($nocheck) {
        if (/\/\* *END *JSSTYLED *\*\//) {
            $nocheck = 0;
        } else {
            reset_indent();
            next line;
        }
    }

    # a /*JSSTYLED*/ comment indicates that the next line is ok.
    if ($nextok) {
        if ($okmsg) {
            err($okmsg);
        }
        $nextok = 0;
        $okmsg = 0;
        if (/\/\* *JSSTYLED.*\*\//) {
            /^.*\/\* *JSSTYLED *(.*) *\*\/.*$/;
            $okmsg = $1;
            $nextok = 1;
        }
        $no_errs = 1;
    } elsif ($no_errs) {
        $no_errs = 0;
    }

    # check length of line.
    # first, a quick check to see if there is any chance of being too long.
    if ((($line =~ tr/\t/\t/) * ($tab_width - 1)) + length($line) > $config{"line-length"}) {
        # yes, there is a chance.
        # replace tabs with spaces and check again.
        my $eline = $line;
        1 while $eline =~
            s/\t+/' ' x
            (length($&) * $tab_width - length($`) % $tab_width)/e;
        if (length($eline) > $config{"line-length"}) {
            err("line > " . $config{"line-length"} . " characters");
        }
    }

    # ignore NOTE(...) annotations (assumes NOTE is on lines by itself).
    if ($note_level || /\b_?NOTE\s*\(/) { # if in NOTE or this is NOTE
        s/[^()]//g;           # eliminate all non-parens
        $note_level += s/\(//g - length;  # update paren nest level
        next;
    }

    # a /* BEGIN JSSTYLED */ comment starts a no-check block.
    if (/\/\* *BEGIN *JSSTYLED *\*\//) {
        $nocheck = 1;
    }

    # a /*JSSTYLED*/ comment indicates that the next line is ok.
    if (/\/\* *JSSTYLED.*\*\//) {
        /^.*\/\* *JSSTYLED *(.*) *\*\/.*$/;
        $okmsg = $1;
        $nextok = 1;
    }
    if (/\/\/ *JSSTYLED/) {
        /^.*\/\/ *JSSTYLED *(.*)$/;
        $okmsg = $1;
        $nextok = 1;
    }

    # universal checks; apply to everything
    if (/\t +\t/) {
        err("spaces between tabs");
    }
    if (/ \t+ /) {
        err("tabs between spaces");
    }
    if (/\s$/) {
        err("space or tab at end of line");
    }
    if (/[^ \t(]\/\*/ && !/\w\(\/\*.*\*\/\);/) {
        err("comment preceded by non-blank");
    }

    # is this the beginning or ending of a function?
    # (not if "struct foo\n{\n")
    if (/^{$/ && $prev =~ /\)\s*(const\s*)?(\/\*.*\*\/\s*)?\\?$/) {
        $in_function = 1;
        $in_declaration = 1;
        $in_function_header = 0;
        $prev = $line;
        next line;
    }
    if (/^}\s*(\/\*.*\*\/\s*)*$/) {
        if ($prev =~ /^\s*return\s*;/) {
            err_prev("unneeded return at end of function");
        }
        $in_function = 0;
        reset_indent();     # we don't check between functions
        $prev = $line;
        next line;
    }
    if (/^\w*\($/) {
        $in_function_header = 1;
    }

    # a blank line terminates the declarations within a function.
    # XXX - but still a problem in sub-blocks.
    if ($in_declaration && /^$/) {
        $in_declaration = 0;
    }

    if ($comment_done) {
        $in_comment = 0;
        $in_header_comment = 0;
        $comment_done = 0;
    }
    # does this looks like the start of a block comment?
    if (/$hdr_comment_start/) {
        if ($config{"indent"} eq "tab") {
            if (!/^\t*\/\*/) {
                err("block comment not indented by tabs");
            }
        } elsif (!/^ *\/\*/) {
            err("block comment not indented by spaces");
        }
        $in_comment = 1;
        /^(\s*)\//;
        $comment_prefix = $1;
        if ($comment_prefix eq "") {
            $in_header_comment = 1;
        }
        $prev = $line;
        next line;
    }
    # are we still in the block comment?
    if ($in_comment) {
        if (/^$comment_prefix \*\/$/) {
            $comment_done = 1;
        } elsif (/\*\//) {
            $comment_done = 1;
            err("improper block comment close")
                unless ($ignore_hdr_comment && $in_header_comment);
        } elsif (!/^$comment_prefix \*[ \t]/ &&
            !/^$comment_prefix \*$/) {
            err("improper block comment")
                unless ($ignore_hdr_comment && $in_header_comment);
        }
    }

    if ($in_header_comment && $ignore_hdr_comment) {
        $prev = $line;
        next line;
    }

    # check for errors that might occur in comments and in code.

    # allow spaces to be used to draw pictures in header comments.
    #if (/[^ ]     / && !/".*     .*"/ && !$in_header_comment) {
    #   err("spaces instead of tabs");
    #}
    #if (/^ / && !/^ \*[ \t\/]/ && !/^ \*$/ &&
    #    (!/^    \w/ || $in_function != 0)) {
    #   err("indent by spaces instead of tabs");
    #}
    if ($config{"indent"} eq "tab") {
        if (/^ {2,}/ && !/^    [^ ]/) {
            err("indent by spaces instead of tabs");
        }
    } elsif (/^\t/) {
        err("indent by tabs instead of spaces")
    } elsif (/^( +)/ && !$in_comment) {
        my $indent = $1;
        if (length($indent) < $config{"indent"}) {
            err("indent of " . length($indent) .
                " space(s) instead of " . $config{"indent"});
        } elsif ($config{"strict-indent"} &&
            length($indent) % $config{"indent"} != 0) {
            err("indent is " . length($indent) .
                " not a multiple of " . $config{'indent'} . " spaces");
        }
    }
    if (/^\t+ [^ \t\*]/ || /^\t+  \S/ || /^\t+   \S/) {
        err("continuation line not indented by 4 spaces");
    }

    # A multi-line block comment must not have content on the first line.
    if (/^\s*\/\*./ && !/^\s*\/\*.*\*\// && !/$hdr_comment_start/) {
        err("improper first line of block comment");
    }

    if ($in_comment) {  # still in comment, don't do further checks
        $prev = $line;
        next line;
    }

    if ($config{"blank-after-open-comment"} && (/[^(]\/\*\S/ || /^\/\*\S/) &&
        !(/$lint_re/ || ($config{"splint"} && /$splint_re/))) {
        err("missing blank after open comment");
    }
    if (/\S\*\/[^)]|\S\*\/$/ &&
        !(/$lint_re/ || ($config{"splint"} && /$splint_re/))) {
        err("missing blank before close comment");
    }
    if ($config{"blank-after-start-comment"} && /(?<!\w:)\/\/\S/) { # C++ comments
        err("missing blank after start comment");
    }
    # check for unterminated single line comments, but allow them when
    # they are used to comment out the argument list of a function
    # declaration.
    if (/\S.*\/\*/ && !/\S.*\/\*.*\*\// && !/\(\/\*/) {
        err("unterminated single line comment");
    }

    if (/^(#else|#endif|#include)(.*)$/) {
        $prev = $line;
        next line;
    }

    #
    # delete any comments and check everything else.  Note that
    # ".*?" is a non-greedy match, so that we don't get confused by
    # multiple comments on the same line.
    #
    s/\/\*.*?\*\///g;
    s/\/\/.*$//;       # C++ comments

    # delete any trailing whitespace; we have already checked for that.
    s/\s*$//;

    # following checks do not apply to text in comments.
    my $quote = $config{"literal-string-quote"};
    if ($quote eq "single") {
        if (/"/) {
            err("literal string using double-quote instead of single");
        }
    } elsif ($quote eq "double") {
        if (/'/) {
            err("literal string using single-quote instead of double");
        }
    }

    if (/[^=!<>\s][!<>=]=/ || /[^<>!=][!<>=]==?[^\s,=]/ ||
        (/[^->]>[^,=>\s]/ && !/[^->]>$/) ||
        (/[^<]<[^,=<\s]/ && !/[^<]<$/) ||
        /[^<\s]<[^<]/ || /[^->\s]>[^>]/) {
        err("missing space around relational operator");
    }
    if (/\S>>=/ || /\S<<=/ || />>=\S/ || /<<=\S/ || /\S[-+*\/&|^%]=/ ||
        (/[^-+*\/&|^%!<>=\s]=[^=]/ && !/[^-+*\/&|^%!<>=\s]=$/) ||
        (/[^!<>=]=[^=\s]/ && !/[^!<>=]=$/)) {
        # XXX - should only check this for C++ code
        # XXX - there are probably other forms that should be allowed
        if (!/\soperator=/) {
            err("missing space around assignment operator");
        }
    }
    if (/[,;]\S/ && !/\bfor \(;;\)/ &&
        # Allow a comma in a regex quantifier.
        !/\/.*?\{\d+,?\d*\}.*?\//) {
        err("comma or semicolon followed by non-blank");
    }
    # check for commas preceded by blanks
    if ((!$config{"leading-comma-ok"} && /^\s*,/) || (!/^\s*,/ && /\s,/)) {
        err("comma preceded by blank");
    }
    # check for semicolons preceded by blanks
    # allow "for" statements to have empty "while" clauses
    if (/\s;/ && !/^[\t]+;$/ && !/^\s*for \([^;]*; ;[^;]*\)/) {
        err("semicolon preceded by blank");
    }
    if (!$config{"continuation-at-front"} && /^\s*(&&|\|\|)/) {
        err("improper boolean continuation");
    } elsif ($config{"continuation-at-front"} && /(&&|\|\||\+)$/) {
        err("improper continuation");
    }
    if (/\S   *(&&|\|\|)/ || /(&&|\|\|)   *\S/) {
        err("more than one space around boolean operator");
    }
    # We allow methods which look like obj.delete() but not keywords without
    # spaces ala: delete(obj)
    if (!$config{"no-blank-for-anon-function"} && /(?<!\.)\bfunction\(/) {
        err("missing space between 'function' and paren");
    } elsif ($config{"no-blank-for-anon-function"} && /(?<!\.)\bfunction\s+\(/) {
        err("space between 'function' and paren");
    }
    if (/(?<!\.)\b(delete|typeof|instanceof|throw|with|catch|new|in|for|if|while|switch|return|case)\(/) {
        err("missing space between keyword and paren");
    }
    if (/(\b(catch|for|if|with|while|switch|return)\b.*){2,}/) {
        # multiple "case" and "sizeof" allowed
        err("more than one keyword on line");
    }
    if (/\b(delete|typeof|instanceOf|with|throw|catch|new|function|in|for|if|while|switch|return|case)\s\s+\(/ &&
        !/^#if\s+\(/) {
        err("extra space between keyword and paren");
    }
    # try to detect "func (x)" but not "if (x)" or
    # "#define foo (x)" or "int (*func)();"
    if (/\w\s\(/) {
        my $s = $_;
        # strip off all keywords on the line
        s/\b(delete|typeof|instanceOf|throw|with|catch|new|function|in|for|if|while|switch|return|case)\s\(/XXX(/g;
        s/#elif\s\(/XXX(/g;
        s/^#define\s+\w+\s+\(/XXX(/;
        # do not match things like "void (*f)();"
        # or "typedef void (func_t)();"
        s/\w\s\(+\*/XXX(*/g;
        s/\b(void)\s+\(+/XXX(/og;
        if (/\w\s\(/) {
            err("extra space between function name and left paren");
        }
        $_ = $s;
    }

    if ($config{"unparenthesized-return"} &&
        /^\s*return\W[^;]*;/ && !/^\s*return\s*\(.*\);/) {
        err("unparenthesized return expression");
    }
    if (/\btypeof\b/ && !/\btypeof\s*\(.*\)/) {
        err("unparenthesized typeof expression");
    }
    if (!$config{"whitespace-after-left-paren-ok"} && /\(\s/) {
        err("whitespace after left paren");
    }
    # allow "for" statements to have empty "continue" clauses
    if (/\s\)/ && !/^\s*for \([^;]*;[^;]*; \)/) {
        if ($config{"leading-right-paren-ok"} && /^\s+\)/) {
            # this is allowed
        } else {
            err("whitespace before right paren");
        }
    }
    if (/^\s*\(void\)[^ ]/) {
        err("missing space after (void) cast");
    }
    if (/\S{/ && !/({|\(){/ &&
        # Allow a brace in a regex quantifier.
        !/\/.*?\{\d+,?\d*\}.*?\//) {
        err("missing space before left brace");
    }
    if ($in_function && /^\s+{/ &&
        ($prev =~ /\)\s*$/ || $prev =~ /\bstruct\s+\w+$/)) {
        err("left brace starting a line");
    }
    if (/}(else|while)/) {
        err("missing space after right brace");
    }
    if (/}\s\s+(else|while)/) {
        err("extra space after right brace");
    }
    if (/^\s+#/) {
        err("preprocessor statement not in column 1");
    }
    if (/^#\s/) {
        err("blank after preprocessor #");
    }

    #
    # We completely ignore, for purposes of indentation:
    #  * lines outside of functions
    #  * preprocessor lines
    #
    if ($check_continuation && $in_function && !$in_cpp) {
        process_indent($_);
    }

    if (/^\s*else\W/) {
        if (!$config{"uncuddled-else-ok"} && $prev =~ /^\s*}$/) {
            err_prefix($prev,
                "else and right brace should be on same line");
        }
    }
    $prev = $line;
}

if ($prev eq "") {
    err("last line in file is blank");
}

}

#
# Continuation-line checking
#
# The rest of this file contains the code for the continuation checking
# engine.  It's a pretty simple state machine which tracks the expression
# depth (unmatched '('s and '['s).
#
# Keep in mind that the argument to process_indent() has already been heavily
# processed; all comments have been replaced by control-A, and the contents of
# strings and character constants have been elided.
#

my $cont_in;        # currently inside of a continuation
my $cont_off;       # skipping an initializer or definition
my $cont_noerr;     # suppress cascading errors
my $cont_start;     # the line being continued
my $cont_base;      # the base indentation
my $cont_first;     # this is the first line of a statement
my $cont_multiseg;  # this continuation has multiple segments

my $cont_special;   # this is a C statement (if, for, etc.)
my $cont_macro;     # this is a macro
my $cont_case;      # this is a multi-line case

my @cont_paren;     # the stack of unmatched ( and [s we've seen

sub
reset_indent()
{
    $cont_in = 0;
    $cont_off = 0;
}

sub
delabel($)
{
    #
    # replace labels with tabs.  Note that there may be multiple
    # labels on a line.
    #
    local $_ = $_[0];

    while (/^(\t*)( *(?:(?:\w+\s*)|(?:case\b[^:]*)): *)(.*)$/) {
        my ($pre_tabs, $label, $rest) = ($1, $2, $3);
        $_ = $pre_tabs;
        while ($label =~ s/^([^\t]*)(\t+)//) {
            $_ .= "\t" x (length($2) + length($1) / 8);
        }
        $_ .= ("\t" x (length($label) / 8)).$rest;
    }

    return ($_);
}

sub
process_indent($)
{
    require strict;
    local $_ = $_[0];           # preserve the global $_

    s///g; # No comments
    s/\s+$//;   # Strip trailing whitespace

    return          if (/^$/);  # skip empty lines

    # regexps used below; keywords taking (), macros, and continued cases
    my $special = '(?:(?:\}\s*)?else\s+)?(?:if|for|while|switch)\b';
    my $macro = '[A-Z_][A-Z_0-9]*\(';
    my $case = 'case\b[^:]*$';

    # skip over enumerations, array definitions, initializers, etc.
    if ($cont_off <= 0 && !/^\s*$special/ &&
        (/(?:(?:\b(?:enum|struct|union)\s*[^\{]*)|(?:\s+=\s*)){/ ||
        (/^\s*{/ && $prev =~ /=\s*(?:\/\*.*\*\/\s*)*$/))) {
        $cont_in = 0;
        $cont_off = tr/{/{/ - tr/}/}/;
        return;
    }
    if ($cont_off) {
        $cont_off += tr/{/{/ - tr/}/}/;
        return;
    }

    if (!$cont_in) {
        $cont_start = $line;

        if (/^\t* /) {
            err("non-continuation indented 4 spaces");
            $cont_noerr = 1;        # stop reporting
        }
        $_ = delabel($_);   # replace labels with tabs

        # check if the statement is complete
        return      if (/^\s*\}?$/);
        return      if (/^\s*\}?\s*else\s*\{?$/);
        return      if (/^\s*do\s*\{?$/);
        return      if (/{$/);
        return      if (/}[,;]?$/);

        # Allow macros on their own lines
        return      if (/^\s*[A-Z_][A-Z_0-9]*$/);

        # cases we don't deal with, generally non-kosher
        if (/{/) {
            err("stuff after {");
            return;
        }

        # Get the base line, and set up the state machine
        /^(\t*)/;
        $cont_base = $1;
        $cont_in = 1;
        @cont_paren = ();
        $cont_first = 1;
        $cont_multiseg = 0;

        # certain things need special processing
        $cont_special = /^\s*$special/? 1 : 0;
        $cont_macro = /^\s*$macro/? 1 : 0;
        $cont_case = /^\s*$case/? 1 : 0;
    } else {
        $cont_first = 0;

        # Strings may be pulled back to an earlier (half-)tabstop
        unless ($cont_noerr || /^$cont_base    / ||
            (/^\t*(?:    )?(?:gettext\()?\"/ && !/^$cont_base\t/)) {
            err_prefix($cont_start,
                "continuation should be indented 4 spaces");
        }
    }

    my $rest = $_;          # keeps the remainder of the line

    #
    # The split matches 0 characters, so that each 'special' character
    # is processed separately.  Parens and brackets are pushed and
    # popped off the @cont_paren stack.  For normal processing, we wait
    # until a ; or { terminates the statement.  "special" processing
    # (if/for/while/switch) is allowed to stop when the stack empties,
    # as is macro processing.  Case statements are terminated with a :
    # and an empty paren stack.
    #
    foreach $_ (split /[^\(\)\[\]\{\}\;\:]*/) {
        next        if (length($_) == 0);

        # rest contains the remainder of the line
        my $rxp = "[^\Q$_\E]*\Q$_\E";
        $rest =~ s/^$rxp//;

        if (/\(/ || /\[/) {
            push @cont_paren, $_;
        } elsif (/\)/ || /\]/) {
            my $cur = $_;
            tr/\)\]/\(\[/;

            my $old = (pop @cont_paren);
            if (!defined($old)) {
                err("unexpected '$cur'");
                $cont_in = 0;
                last;
            } elsif ($old ne $_) {
                err("'$cur' mismatched with '$old'");
                $cont_in = 0;
                last;
            }

            #
            # If the stack is now empty, do special processing
            # for if/for/while/switch and macro statements.
            #
            next        if (@cont_paren != 0);
            if ($cont_special) {
                if ($rest =~ /^\s*{?$/) {
                    $cont_in = 0;
                    last;
                }
                if ($rest =~ /^\s*;$/) {
                    err("empty if/for/while body ".
                        "not on its own line");
                    $cont_in = 0;
                    last;
                }
                if (!$cont_first && $cont_multiseg == 1) {
                    err_prefix($cont_start,
                        "multiple statements continued ".
                        "over multiple lines");
                    $cont_multiseg = 2;
                } elsif ($cont_multiseg == 0) {
                    $cont_multiseg = 1;
                }
                # We've finished this section, start
                # processing the next.
                goto section_ended;
            }
            if ($cont_macro) {
                if ($rest =~ /^$/) {
                    $cont_in = 0;
                    last;
                }
            }
        } elsif (/\;/) {
            if ($cont_case) {
                err("unexpected ;");
            } elsif (!$cont_special) {
                err("unexpected ;") if (@cont_paren != 0);
                if (!$cont_first && $cont_multiseg == 1) {
                    err_prefix($cont_start,
                        "multiple statements continued ".
                        "over multiple lines");
                    $cont_multiseg = 2;
                } elsif ($cont_multiseg == 0) {
                    $cont_multiseg = 1;
                }
                if ($rest =~ /^$/) {
                    $cont_in = 0;
                    last;
                }
                if ($rest =~ /^\s*special/) {
                    err("if/for/while/switch not started ".
                        "on its own line");
                }
                goto section_ended;
            }
        } elsif (/\{/) {
            err("{ while in parens/brackets") if (@cont_paren != 0);
            err("stuff after {")        if ($rest =~ /[^\s}]/);
            $cont_in = 0;
            last;
        } elsif (/\}/) {
            err("} while in parens/brackets") if (@cont_paren != 0);
            if (!$cont_special && $rest !~ /^\s*(while|else)\b/) {
                if ($rest =~ /^$/) {
                    err("unexpected }");
                } else {
                    err("stuff after }");
                }
                $cont_in = 0;
                last;
            }
        } elsif (/\:/ && $cont_case && @cont_paren == 0) {
            err("stuff after multi-line case") if ($rest !~ /$^/);
            $cont_in = 0;
            last;
        }
        next;
section_ended:
        # End of a statement or if/while/for loop.  Reset
        # cont_special and cont_macro based on the rest of the
        # line.
        $cont_special = ($rest =~ /^\s*$special/)? 1 : 0;
        $cont_macro = ($rest =~ /^\s*$macro/)? 1 : 0;
        $cont_case = 0;
        next;
    }
    $cont_noerr = 0         if (!$cont_in);
}
